﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using SlamCms.Caching;
using SlamCms.Common;
using SlamCms.Data;
using SlamCms.Logging;
using SlamCms.Profiling;
using SlamCms.Web;

namespace SlamCms.Services
{
	public class DefaultServices : ServicesContainer
	{
		// This lock protects both caches (and _lastKnownDependencyResolver is updated under it as well)
		private readonly ReaderWriterLockSlim _cacheLock = new ReaderWriterLockSlim();
		private readonly Dictionary<Type, object[]> _cacheMulti = new Dictionary<Type, object[]>();
		private readonly Dictionary<Type, object> _cacheSingle = new Dictionary<Type, object>();
		private readonly SlamCmsConfiguration _configuration;

		// Mutation operations delegate (throw if applied to wrong set)
		private readonly Dictionary<Type, object> _defaultServicesSingle = new Dictionary<Type, object>();

		private readonly Dictionary<Type, List<object>> _defaultServicesMulti = new Dictionary<Type, List<object>>();
		private IDependencyResolver _lastKnownDependencyResolver;
		private readonly HashSet<Type> _serviceTypesSingle;
		private readonly HashSet<Type> _serviceTypesMulti;

		/// <summary>
		/// This constructor is for unit testing purposes only.
		/// </summary>
		protected DefaultServices()
		{
		}

		private void SetSingle<T>(T instance) where T : class
		{
			_defaultServicesSingle[typeof(T)] = instance;
		}
		private void SetMultiple<T>(params T[] instances) where T : class
		{
			var x = (IEnumerable<object>)instances;
			_defaultServicesMulti[typeof(T)] = new List<object>(x);
		}

		public DefaultServices(SlamCmsConfiguration configuration)
		{
			Ensure.ArgumentNotNull(configuration, "configuration");
			
			_configuration = configuration;

			// Initialize the dictionary with all known service types, even if the list for that service type is
			// empty, because we will throw if the developer tries to read or write unsupported types.

			SetSingle<ICacheManager>(new DefaultCacheManager());
			SetSingle<IDbConnectionResolver>(new DefaultDbConnectionResolver());
			SetSingle<ILogManager>(new DefaultLogManager());
			SetSingle<IFormsAuthentication>(new FormsAuthenticationWrapper());
			SetSingle<IProfiler>(new DefaultProfiler());
		
			//ModelValidatorCache validatorCache = new ModelValidatorCache(new Lazy<IEnumerable<ModelValidatorProvider>>(() => this.GetModelValidatorProviders()));
			//configuration.RegisterForDispose(validatorCache);
			//SetSingle<IModelValidatorCache>(validatorCache);

			_serviceTypesSingle = new HashSet<Type>(_defaultServicesSingle.Keys);
			_serviceTypesMulti = new HashSet<Type>(_defaultServicesMulti.Keys);

			// Reset the caches and the known dependency scope
			ResetCache();
		}

		public override bool IsSingleService(Type serviceType)
		{
			Ensure.ArgumentNotNull(serviceType, "serviceType");
			return _serviceTypesSingle.Contains(serviceType);
		}

		/// <inheritdoc/>
		public override void Dispose()
		{
			_cacheLock.Dispose();
		}

		/// <summary>
		/// Try to get a service of the given type.
		/// </summary>
		/// <param name="serviceType">The service type.</param>
		/// <returns>The first instance of the service, or null if the service is not found.</returns>
		public override object GetService(Type serviceType)
		{
			Ensure.ArgumentNotNull(serviceType, "serviceType");
			
			if (!_serviceTypesSingle.Contains(serviceType))
			{
				throw Error.Argument("serviceType", "Invalid service type {0}", serviceType.Name);
			}

			// Invalidate the cache if the dependency scope has switched
			if (_lastKnownDependencyResolver != _configuration.DependencyResolver)
			{
				ResetCache();
			}

			object result;

			_cacheLock.EnterReadLock();
			try
			{
				if (_cacheSingle.TryGetValue(serviceType, out result))
				{
					return result;
				}
			}
			finally
			{
				_cacheLock.ExitReadLock();
			}

			// Get the service from DI, outside of the lock. If we're coming up hot, this might
			// mean we end up creating the service more than once.
			object dependencyService = _configuration.DependencyResolver.GetService(serviceType);

			_cacheLock.EnterWriteLock();
			try
			{
				if (!_cacheSingle.TryGetValue(serviceType, out result))
				{
					result = dependencyService ?? _defaultServicesSingle[serviceType];
					_cacheSingle[serviceType] = result;
				}

				return result;
			}
			finally
			{
				_cacheLock.ExitWriteLock();
			}
		}

		/// <summary>
		/// Try to get a list of services of the given type.
		/// </summary>
		/// <param name="serviceType">The service type.</param>
		/// <returns>The list of service instances of the given type. Returns an empty enumeration if the
		/// service is not found. </returns>
		public override IEnumerable<object> GetServices(Type serviceType)
		{
			Ensure.ArgumentNotNull(serviceType, "serviceType");
			
			if (!_serviceTypesMulti.Contains(serviceType))
			{
				throw Error.Argument("serviceType", "Invalid service type {0}", serviceType.Name);
			}

			// Invalidate the cache if the dependency scope has switched
			if (_lastKnownDependencyResolver != _configuration.DependencyResolver)
			{
				ResetCache();
			}

			object[] result;

			_cacheLock.EnterReadLock();
			try
			{
				if (_cacheMulti.TryGetValue(serviceType, out result))
				{
					return result;
				}
			}
			finally
			{
				_cacheLock.ExitReadLock();
			}

			// Get the service from DI, outside of the lock. If we're coming up hot, this might
			// mean we end up creating the service more than once.
			IEnumerable<object> dependencyServices = _configuration.DependencyResolver.GetServices(serviceType);

			_cacheLock.EnterWriteLock();
			try
			{
				if (!_cacheMulti.TryGetValue(serviceType, out result))
				{
					result = dependencyServices.Where(s => s != null)
											   .Concat(_defaultServicesMulti[serviceType])
											   .ToArray();
					_cacheMulti[serviceType] = result;
				}

				return result;
			}
			finally
			{
				_cacheLock.ExitWriteLock();
			}
		}

		// Returns the List<object> for the given service type. Also validates serviceType is in the known service type list.
		protected override List<object> GetServiceInstances(Type serviceType)
		{
			Ensure.ArgumentNotNull(serviceType, "serviceType");

			List<object> result;
			if (!_defaultServicesMulti.TryGetValue(serviceType, out result))
			{
				throw Error.Argument("serviceType", "Invalid service type {0}", serviceType.Name);
			}

			return result;
		}

		protected override void ClearSingle(Type serviceType)
		{
			_defaultServicesSingle[serviceType] = null;
		}

		protected override void ReplaceSingle(Type serviceType, object service)
		{
			Ensure.ArgumentNotNull(serviceType, "serviceType");
			_defaultServicesSingle[serviceType] = service;
		}

		// Removes the cached values for all service types. Called when the dependency scope
		// has changed since the last time we made a request.
		private void ResetCache()
		{
			_cacheLock.EnterWriteLock();
			try
			{
				_cacheSingle.Clear();
				_cacheMulti.Clear();
				_lastKnownDependencyResolver = _configuration.DependencyResolver;
			}
			finally
			{
				_cacheLock.ExitWriteLock();
			}
		}

		// Removes the cached values for a single service type. Called whenever the user manipulates
		// the local service list for a given service type.
		protected override void ResetCache(Type serviceType)
		{
			_cacheLock.EnterWriteLock();
			try
			{
				_cacheSingle.Remove(serviceType);
				_cacheMulti.Remove(serviceType);
			}
			finally
			{
				_cacheLock.ExitWriteLock();
			}
		}
	}
}
